耶,第十三天,昨天講了goroutine,今天就來講channel吧!你說這兩個有什麼關聯?我一開始也想了很久,希望今天的文章可以解釋到
queue
<-將資料放入與取出channel內沒有東西,程式會等待另一端操作完ch := make(chan int , n) // 建立 int 型別長度為 n 的 Channel
使用 <- 來操作資料,想像channel為管道,右進左出,ch<-data為將data放入,<-ch為取出資料
ch := make(chan int , 1)
ch <- 5
fmt.Println(<-ch) // 5
可利用len()與cap()來確認裡面有多少筆資料與容量,
ch := make(chan int , 1) // (len(ch),cap(ch)) = 0,1
ch <- 5 // (len(ch),cap(ch)) = 1,1
fmt.Println(<-ch) // 5 // (len(ch),cap(ch)) = 0,1
可以看出5被放入時len=1,取出後len=0
Channel在推入與拉出資料時都有可能發生阻塞問題
channel時,在這筆資料拉出之前,其他的ch<-data會被卡住ch內沒資料時,<-ch會卡住,直到channel內有資料時才執行推入資料的等待情境
func main() {
ch := make(chan int, 1)
ch <- 1
ch <- 2
fmt.Println("Hi")
}
卡在 ch <- 2 這行,程式直接暫停
拉出資料的等待情境
func main() {
ch := make(chan int)
fmt.Println(<-ch)
fmt.Println("Hi")
}
長度為0,資料放不進去,<-ch這邊卡住,又暫停了
好啦,那這個像水管一樣的資料型態,什麼時候可以用上?從昨天的goroutine與Race Condition開始講好了。
競爭條件?這是什麼東西,我們先看一下下面的例子
func main() {
total := 0
for i := 0; i < 1000; i++ {
go func(){
total++
}()
}
time.Sleep(time.Second)
fmt.Println(total)
}
output:
982
輸出怎麼不是1000?其實也不一定是982,每次執行數字都會浮動。那是因為goroutine在為total進行++時,說不定還沒處理完並存成新的total,原先的goroutine就又進入了迴圈並取得了total去做運算,這樣雖然跑了兩次++,但其實total只加了一次,這是多執行續時一定會遇上的問題
帶數字舉例。total此時是50
go func去做total=total+1
main又進了迴圈一次,跑go func
goroutine2取得的total還是50goroutine1與goroutine2同時運算完,兩個都回傳total=51
多執行續偶爾會遇到這種變數問題。這就是為什麼上面的答案會與預期有落差。
這在作業系統上稱做Race Condition,有更簡單的例子啦,只是我想結合goroutine一起講,可以查一下其他Race Condition的例子。
有種做法是為變數上鎖,當有人在使用時就禁止別人使用,Go可以使用互斥鎖來實做這個方法。
type SafeNumber struct {
v int
mux sync.Mutex // 互斥鎖
}
func main() {
total := SafeNumber{v:0}
for i := 0; i < 1000; i++ {
go func(){
total.mux.Lock()
total.v++
total.mux.Unlock()
}()
}
time.Sleep(time.Second)
total.mux.Lock()
fmt.Println(total.v)
total.mux.Unlock()
}
output:
1000
SafeNumber結構,內有sync.Mutex互斥鎖SafeNumber.v的前後都使用Lock、Unlock上鎖與開鎖,確保同一時間只有一執行續在操作變數。Go提供了channel這個資料型態來完成操作共同變數的功能,Go官方文章說明了這點。
同個例子改成使用channel
func main() {
ch := make(chan int, 1)
ch <- 0
for i := 0; i < 1000; i++ {
go func() {
ch <- (<-ch + 1)
}()
}
time.Sleep(time.Second)
fmt.Println(<-ch)
}
output:
1000
每一個go func執行順序如下
<-ch 拉出ch內的資料(<-ch + 1) 將拉出來的資料+1ch <- (<-ch + 1) 推回ch因此就算goroutine2趕上了goroutine1,只要goroutine1還沒將資料放回ch,goroutine2就會進入等待期,避免搶奪問題發生
Channel又分為這兩種。Buffered,緩衝區,顧名思義就是有沒有緩衝區的channel而已。在宣告時會一併宣告channel大小,若沒輸入預設長度為0。
Unbuffered Channel: 在使用上若有東西塞入channel,一定要靠另一個goroutine將東西拉出去後才有辦法繼續執行,否則會卡住Buffered Channel: 可將東西儲存至容量上限,等待另一端將資料取出在goroutine進入等待期時使用者是不清楚的,因此可以使用select讓程式在進入等待期時也能有一點輸出。
ch := make(chan string)
go func() {
time.Sleep(time.Second) //模擬費時運算
ch <- "FINISH"
}()
for {
select {
case (<-ch): // Channel 中有資料執行此區域
fmt.Println("main完成")
return
default: // Channel 阻塞的話執行此區域
fmt.Println("waiting...")
time.Sleep(500 * time.Millisecond)
}
}
output:
waiting...
waiting...
waiting...
main完成
這種方式就能在等待期也輸出一點資料
完成啦,又完成了一篇耗時的文章,跟昨天一樣卡了很久看不懂到底要channel幹嘛,明明原本的資料型態就夠了幹嘛還多學一個。不過感覺與goroutine一樣以後在寫系統相關程式或微服務時應該都會用到,先學起來以後用吧
Go 的並發:Goroutine 與 Channel 介紹
https://peterhpchen.github.io/2020/03/08/goroutine-and-channel.html
olang 教學系列 - 何謂 Channel? 先從宣告 Channel 開始學起!
https://blog.kennycoder.io/2020/12/23/Golang%E6%95%99%E5%AD%B8%E7%B3%BB%E5%88%97-%E4%BD%95%E8%AC%82Channel-%E5%85%88%E5%BE%9E%E5%AE%A3%E5%91%8AChannel%E9%96%8B%E5%A7%8B%E5%AD%B8%E8%B5%B7/